Design Vending Machine

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low level design of a vending machine in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.

Here is an example of how a discussion between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Accept coin-based payments using fixed denominations (e.g., $1, $5, $10)
  • Allow adding new items or restocking existing items in the vending machine
  • Allow users to view available items along with their price
  • Users can select an item by entering the item code
  • The machine should dispense the selected item if sufficient money is inserted
  • Return change if the inserted amount exceeds the item’s price
  • Allow users to cancel a transaction before item dispensing and receive a full refund
  • Display intermediate states such as inserted amount, selected item, or refund messages

1.2 Non-Functional Requirements

  • Maintainability: The system should follow object-oriented principles, ensuring modularity, testability, and ease of extension
  • Atomicity: The purchase operation must be atomic. A user either receives the item and correct change, or receives a full refund
  • Concurrency Control: The machine must handle only one transaction at a time. While a transaction is in progress, the system should remain locked to other inputs
  • Extensibility: The system should be designed in a way that future features (e.g., digital payments) can be added with minimal changes

After the requirements are clear, lets identify the core entities/objects we will have in our system.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., item, coin, transaction, display, vending machine) and actions (e.g., select, dispense, refund, restock) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.

Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they represent the same conceptual unit.

1. Accept coin-based payments using fixed denominations

This introduces:

  • Coin (enum): Represents valid coin denominations accepted by the machine (e.g., $1, $5, $10). Using an enum helps enforce a fixed set of supported denominations and simplifies validation and change calculation.

2. The system should allow adding new items and restocking.

This introduces:

  • Item: Represents a product sold by the machine. Each item has attributes such as code, name, price, and quantity.
  • Inventory: Manages the machine’s stock of items. It maintains a mapping of item codes to their corresponding Item objects and provides operations to add, restock, reduce, and check availability.

3. Machine accepts coins, dispenses items and returns change.

This introduces:

  • VendingMachine: The central orchestrator that coordinates all user interactions and internal processes. It manages the current balance, selected item code, coin validation, transaction flow, state transitions, and interactions with the inventory.

These core entities define the essential abstractions of the vending machine system and will guide the structure of our low-level design and class diagrams.

3. Designing Classes and Relationships

3.1 Class Definitions

The system is composed of several types of classes, each with a distinct role.

Enums

Enums
  • Coin: Represents the set of valid coin denominations accepted by the machine. Using an enum ensures type safety and centralizes the value of each coin (e.g., PENNY(1), QUARTER(25)), making the system easy to extend with new coin types without changing the core logic.

Data Classes

Item

Item

A simple Plain Old Java Object (POJO) or data class that models a product. It holds product-specific information: a unique code for selection (e.g., "A1"), a name ("Coke"), and a price (in cents). This class has no business logic; its sole purpose is to encapsulate data.

Core Classes

Inventory

This class is responsible for managing the stock of all items.

Inventory

It uses two maps: one to associate an item code with its Item object and another to track the quantity (stock) of each item. Its responsibilities are limited to adding items, checking availability, and reducing stock, adhering to the Single Responsibility Principle.

VendingMachine (The Context)

This is the main class and the primary entry point for any client interaction.

VendingMachine

It holds references to the current state (currentState), the Inventory, the current balance, and the selectedItemCode. It delegates all user actions to the current state object, which handles the request based on the machine's current context.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

  • Composition (VendingMachine has an Inventory): The VendingMachine owns the Inventory. The Inventory cannot exist without the VendingMachine, and its lifecycle is managed by the VendingMachine. This is a strong "has-a" relationship.
  • Aggregation (Inventory has Items): The Inventory manages a collection of Item objects. While the inventory contains items, the Item objects themselves can be considered independent entities. This is a "has-a" relationship, but weaker than composition.
  • Association (VendingMachine has a VendingMachineState): The VendingMachine maintains a reference to its current state object. This reference can change dynamically at runtime, which is the essence of the State pattern. Furthermore, each VendingMachineState object holds a reference back to the VendingMachine to access its context and trigger state transitions.
  • Inheritance (IdleState is a VendingMachineState): The concrete state classes (IdleState, ItemSelectedState, etc.) extend the abstract VendingMachineState class. This enforces a common contract across all states and allows the VendingMachine to treat them polymorphically.

3.3 Key Design Patterns

Several design patterns are employed to create a clean, maintainable, and extensible system.

State Pattern

This is the primary pattern used. It allows the VendingMachine to alter its behavior when its internal state changes.

VendingMachineSta

The machine delegates requests to a state object, which implements the behavior for that specific state. This eliminates the need for large if/else or switch blocks for managing state-dependent logic, making the system cleaner and easier to modify.

  • Context: VendingMachine
  • State: VendingMachineState (abstract class)
  • Concrete States: IdleState, ItemSelectedState, HasMoneyState, DispensingState

Singleton Pattern

The VendingMachine is implemented as a Singleton. This ensures that only one instance of the machine is created throughout the application's lifecycle. This is a logical choice as it models a real-world scenario where you interact with a single, physical machine.

Facade Pattern

The VendingMachine class acts as a Facade. It provides a simple, unified interface (insertCoin(), selectItem(), etc.) to the client. The client interacts with this simplified interface without needing to know about the complex internal subsystems like state management, inventory tracking, or state transition logic.

3.4 Class Diagram

Vending Machine Class Diagram

4. Implementation

4.1 Coin Enum

Represents accepted coin denominations and their values (in cents).

1class Coin(Enum):
2    PENNY = 1
3    NICKEL = 5
4    DIME = 10
5    QUARTER = 25
6
7    def __init__(self, value: int):
8        self.value = value
9
10    def get_value(self) -> int:
11        return self.value

4.2 Item

Models a product available for purchase in the vending machine.

1class Item:
2    def __init__(self, code: str, name: str, price: int):
3        self.code = code
4        self.name = name
5        self.price = price
6
7    def get_name(self) -> str:
8        return self.name
9
10    def get_price(self) -> int:
11        return self.price

Each item has a unique code, name, and price.

4.3 Inventory

This component is responsible for storing and tracking the available items and their quantities.

1class Inventory:
2    def __init__(self):
3        self.item_map: Dict[str, Item] = {}
4        self.stock_map: Dict[str, int] = {}
5
6    def add_item(self, code: str, item: Item, quantity: int) -> None:
7        self.item_map[code] = item
8        self.stock_map[code] = quantity
9
10    def get_item(self, code: str) -> Optional[Item]:
11        return self.item_map.get(code)
12
13    def is_available(self, code: str) -> bool:
14        return self.stock_map.get(code, 0) > 0
15
16    def reduce_stock(self, code: str) -> None:
17        self.stock_map[code] = self.stock_map[code] - 1

The Inventory class has a single, clear purpose: to manage the collection of items and their stock levels. It is decoupled from the machine's operational logic (like handling money or state transitions).

  • addItem() registers new stock.
  • reduceStock() decrements stock after dispensing.

4.4 VendingMachineState Interface and Concrete States

The State pattern allows an object (the VendingMachine) to change its behavior when its internal state changes. The object appears to change its class.

VendingMachineState

This defines the contract for all possible states.

1class VendingMachineState(ABC):
2    def __init__(self, machine):
3        self.machine = machine
4
5    @abstractmethod
6    def insert_coin(self, coin: Coin) -> None:
7        pass
8
9    @abstractmethod
10    def select_item(self, code: str) -> None:
11        pass
12
13    @abstractmethod
14    def dispense(self) -> None:
15        pass
16
17    @abstractmethod
18    def refund(self) -> None:
19        pass

Each class represents a specific state of the vending machine and implements the behavior appropriate for that state.

IdleState

The default state when the machine is waiting for a user to begin an interaction.

1class IdleState(VendingMachineState):
2    def __init__(self, machine):
3        super().__init__(machine)
4
5    def insert_coin(self, coin: Coin) -> None:
6        print("Please select an item before inserting money.")
7
8    def select_item(self, code: str) -> None:
9        if not self.machine.get_inventory().is_available(code):
10            print("Item not available.")
11            return
12        self.machine.set_selected_item_code(code)
13        self.machine.set_state(ItemSelectedState(self.machine))
14        print(f"Item selected: {code}")
15
16    def dispense(self) -> None:
17        print("No item selected.")
18
19    def refund(self) -> None:
20        print("No money to refund.")

In the IdleState, only selectItem is a valid action. All other actions, like insertCoin or dispense, are invalid and result in an error message. A successful selection triggers a state transition to ItemSelectedState.

ItemSelectedState

 The state after a user has selected an item, and the machine is waiting for money.

1class ItemSelectedState(VendingMachineState):
2    def __init__(self, machine):
3        super().__init__(machine)
4
5    def insert_coin(self, coin: Coin) -> None:
6        self.machine.add_balance(coin.get_value())
7        print(f"Coin Inserted: {coin.get_value()}")
8        price = self.machine.get_selected_item().get_price()
9        if self.machine.get_balance() >= price:
10            print("Sufficient money received.")
11            self.machine.set_state(HasMoneyState(self.machine))
12
13    def select_item(self, code: str) -> None:
14        print("Item already selected.")
15
16    def dispense(self) -> None:
17        print("Please insert sufficient money.")
18
19    def refund(self) -> None:
20        self.machine.reset()
21        self.machine.set_state(IdleState(self.machine))

In this state, the primary valid action is insertCoin. The state keeps track of the inserted money and, upon receiving sufficient funds, transitions the machine to the HasMoneyState.

HasMoneyState

 The state when the machine has received enough money for the selected item and is ready to dispense.

1class HasMoneyState(VendingMachineState):
2    def __init__(self, machine):
3        super().__init__(machine)
4
5    def insert_coin(self, coin: Coin) -> None:
6        print("Already received full amount.")
7
8    def select_item(self, code: str) -> None:
9        print("Item already selected.")
10
11    def dispense(self) -> None:
12        self.machine.set_state(DispensingState(self.machine))
13        self.machine.dispense_item()
14
15    def refund(self) -> None:
16        self.machine.refund_balance()
17        self.machine.reset()
18        self.machine.set_state(IdleState(self.machine))
19

he only valid action from the user's perspective is dispense. This action immediately transitions the machine to the DispensingState to prevent any other user interactions during the physical dispensing process.

DispensingState

A transient state that locks the machine while an item is being physically dispensed.

1class DispensingState(VendingMachineState):
2    def __init__(self, machine):
3        super().__init__(machine)
4
5    def insert_coin(self, coin: Coin) -> None:
6        print("Currently dispensing. Please wait.")
7
8    def select_item(self, code: str) -> None:
9        print("Currently dispensing. Please wait.")
10
11    def dispense(self) -> None:
12        # already triggered by HasMoneyState
13        pass
14
15    def refund(self) -> None:
16        print("Dispensing in progress. Refund not allowed.")

This state effectively blocks all user input. The actual dispensing logic is handled by the VendingMachine's dispenseItem method, which, upon completion, will transition the machine back to the IdleState.

4.5 VendingMachine Class (Context)

This class is the main entry point for all client interactions. It holds the current state and delegates actions to it.

1class VendingMachine:
2    _instance = None
3
4    def __new__(cls):
5        if cls._instance is None:
6            cls._instance = super(VendingMachine, cls).__new__(cls)
7            cls._instance._initialized = False
8        return cls._instance
9
10    def __init__(self):
11        if not hasattr(self, '_initialized') or not self._initialized:
12            self.inventory = Inventory()
13            self.current_state = IdleState(self)
14            self.balance = 0
15            self.selected_item_code = None
16            self._initialized = True
17
18    @classmethod
19    def get_instance(cls):
20        return cls()
21
22    def insert_coin(self, coin: Coin) -> None:
23        self.current_state.insert_coin(coin)
24
25    def add_item(self, code: str, name: str, price: int, quantity: int) -> Item:
26        item = Item(code, name, price)
27        self.inventory.add_item(code, item, quantity)
28        return item
29
30    def select_item(self, code: str) -> None:
31        self.current_state.select_item(code)
32
33    def dispense(self) -> None:
34        self.current_state.dispense()
35
36    def dispense_item(self) -> None:
37        item = self.inventory.get_item(self.selected_item_code)
38        if self.balance >= item.get_price():
39            self.inventory.reduce_stock(self.selected_item_code)
40            self.balance -= item.get_price()
41            print(f"Dispensed: {item.get_name()}")
42            if self.balance > 0:
43                print(f"Returning change: {self.balance}")
44        self.reset()
45        self.set_state(IdleState(self))
46
47    def refund_balance(self) -> None:
48        print(f"Refunding: {self.balance}")
49        self.balance = 0
50
51    def reset(self) -> None:
52        self.selected_item_code = None
53        self.balance = 0
54
55    def add_balance(self, value: int) -> None:
56        self.balance += value
57
58    def get_selected_item(self) -> Item:
59        return self.inventory.get_item(self.selected_item_code)
60
61    def set_selected_item_code(self, code: str) -> None:
62        self.selected_item_code = code
63
64    def set_state(self, state: VendingMachineState) -> None:
65        self.current_state = state
66
67    # Getters for states and inventory
68    def get_inventory(self) -> Inventory:
69        return self.inventory
70
71    def get_balance(self) -> int:
72        return self.balance
  • State Pattern "Context": The VendingMachine class is the Context for the State pattern. It holds a reference to the currentState and delegates all user actions to that state object.
  • Facade Pattern: It also acts as a Facade, providing a simple, unified interface (insertCoin, selectItem, etc.) to the client. The client does not need to know about the complex internal states or the inventory management.
  • Singleton Pattern: The machine is implemented as a Singleton (getInstance()) because in the real world, you interact with one single instance of a physical machine.
  • State Transition Control: The machine provides a setState method, allowing the state objects themselves to control the machine's transitions.

4.6 VendingMachineDemo

This driver class demonstrates a complete user journey, showcasing the state transitions in action.

1class VendingMachineDemo:
2    @staticmethod
3    def main():
4        vending_machine = VendingMachine.get_instance()
5
6        # Add products to the inventory
7        vending_machine.add_item("A1", "Coke", 25, 3)
8        vending_machine.add_item("A2", "Pepsi", 25, 2)
9        vending_machine.add_item("B1", "Water", 10, 5)
10
11        # Select a product
12        print("\n--- Step 1: Select an item ---")
13        vending_machine.select_item("A1")
14
15        # Insert coins
16        print("\n--- Step 2: Insert coins ---")
17        vending_machine.insert_coin(Coin.DIME)  # 10
18        vending_machine.insert_coin(Coin.DIME)  # 10
19        vending_machine.insert_coin(Coin.NICKEL)  # 5
20
21        # Dispense the product
22        print("\n--- Step 3: Dispense item ---")
23        vending_machine.dispense()  # Should dispense Coke
24
25        # Select another item
26        print("\n--- Step 4: Select another item ---")
27        vending_machine.select_item("B1")
28
29        # Insert more amount
30        print("\n--- Step 5: Insert more than needed ---")
31        vending_machine.insert_coin(Coin.QUARTER)  # 25
32
33        # Try to dispense the product
34        print("\n--- Step 6: Dispense and return change ---")
35        vending_machine.dispense()
36
37
38if __name__ == "__main__":
39    VendingMachineDemo.main()

5. Run and Test

Languages
Java
C#
Python
C++
Files10
entities
enum
states
inventory.py
vending_machine_demo.py
main
vending_machine.py
vending_machine_demo.py
Output

6. Quiz

Design Vending Machine Quiz

1 / 20
Multiple Choice

Which entity manages the stock of items in a vending machine design?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script